Feature Gates Usage Guide

Feature gates (also known as feature toggles or feature tweaks) allow you to turn pieces of functionality on or off at runtime without shipping a new build. This guide explains what they are, when to use them, and how to implement a new tweak inside the Baselines architecture.


What Is a Feature Gate?

A feature gate is a small abstraction that tells the rest of the app whether a feature is currently available. Under the hood it can talk to remote config, preferences, or any other store, but the public API always looks the same: check if the feature is enabled, optionally flip its value, and react in the UI based on the answer. In Baselines this contract lives in toolkit/feature-tweak module and is modeled by:

  • AppFeature enum
  • FeatureTweak interface
  • Tweaks facade

When Are Feature Tweaks Needed?

Use a tweak whenever you want control over exposing a feature without republishing the app. Typical cases include:

  • Gradually rolling out a capability to internal testers before going GA
  • Keeping experimental UI behind a guard so it can be disabled quickly
  • Building tooling (like the in-app Playground) where product or QA can toggle behaviors while exercising the app

If something must be safe to disable instantly—especially during early development—wrap it in a feature tweak.

Step-by-Step: Implement a New Tweak

Follow these three steps whenever you introduce a new gated feature. The snippets below assume we are adding a Profile feature flag.

1️⃣ Extend FeatureTweak

Create a class that implements FeatureTweak for your feature. The implementation can inject any dependencies it needs (config, storage, etc.) and do any kind of work to make enabled() / tweak(Boolean) functions operate as needed. The code below automatically wires the tweak into the system, so the Tweaks class can pick it up.

kotlin
1@Inject
2@ContributesIntoMap(AppScope::class)
3@FeatureKey(AppFeature.PROFILE)
4// You can add `@SingleIn(AppScope::class)` to make it behave as a singleton,
5// so it can store your runtime variables in memory, like `cachedState` below.
6// In other cases `@SingleIn(AppScope::class)` is redundant.
7@SingleIn(AppScope::class)
8class ProfileFeatureTweak(
9 private val appConfigManager: AppConfigManager,
10) : FeatureTweak {
11
12 private var cachedState = true
13
14 override suspend fun enabled(): Boolean {
15 val appInfo = appConfigManager.appConfig.first().info
16 return appInfo.debug && cachedState
17 }
18
19 override suspend fun tweak(enabled: Boolean) {
20 cachedState = enabled
21 }
22}

2️⃣ Use Tweaks to read or flip the state

Inject Tweaks wherever the UI needs to react to the feature gate. ViewModels typically read the value inside a mutableState block and invoke tweak() when the user flips a toggle. HomeViewModel and the Playground FeatureTweakViewModel provide concrete examples.

kotlin
1@Inject
2@ContributesIntoMap(AppScope::class, binding<ViewModel>())
3@ViewModelKey(ProfileViewModel::class)
4class ProfileEntryViewModel(
5 private val tweaks: Tweaks,
6) : ViewModel(), Mvvm<ProfileUiEvent, ProfileUiState> {
7
8 private val eventSink = createEventSink(::handleEvent)
9 private val profileEnabledState = mutableState(false) {
10 tweaks.enabled(AppFeature.PROFILE)
11 }
12
13 @Composable
14 override fun state(): ProfileUiState {
15 val enabled by profileEnabledState.collectAsStateWithLifecycle()
16 return ProfileUiState(
17 enabled = enabled,
18 eventSink = eventSink,
19 )
20 }
21
22 private fun handleEvent(event: ProfileUiEvent) {
23 when (event) {
24 ProfileUiEvent.ToggleProfile -> handleToggleProfile()
25 }
26 }
27
28 private fun handleToggleProfile() {
29 launch {
30 val enabled = profileEnabledState.value
31 tweaks.tweak(AppFeature.PROFILE, !enabled)
32 // If the current UI must reflect the updated tweak value,
33 // recreate the state after applying the change.
34 profileEnabledState.recreate()
35 }
36 }
37}

With these pieces in place, the new feature gate becomes available throughout the app and can be controlled through the Playground tweaks UI or any other custom surface.